/*
 *  Copyright 2010 Andrew Kroh
 *
 *  Kroh-IO is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Kroh-IO is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package kroh.io;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * CommDriver implementation. Responsible for loading the
 * native system library and finding the serial devices on the
 * system and registering them with <code>CommPortIdentifer</code>.
 */
public class KrohCommDriver implements CommDriver
{
    private static final Logger logger = LoggerFactory.getLogger(KrohCommDriver.class);
    
    /**
     * Name of the OS from os.name system property.
     */
    private final String osName;
    
    /**
     * Directory holding system devices handles.
     */
    private final String devicesDirectory;

    /**
     * Constructor. Reads the os.name property and get the device
     * directory using the native library.
     */
    public KrohCommDriver()
    {
        osName = System.getProperty("os.name");
        
        // Use the device directory specified in the property
        // if it was used:
        String tempDeviceDir = System.getProperty("kroh.io.device.dir");
        if (tempDeviceDir == null)
        {
            devicesDirectory = getOsDeviceDirectory(osName);
        }
        else
        {
            devicesDirectory = tempDeviceDir.trim();
        }
        
        logger.trace("OS Name: <" + osName + ">.");
        logger.trace("Device Directory: <" + devicesDirectory + ">.");
    }

    /* (non-Javadoc)
     * @see gnu.io.CommDriver#initialize()
     */
    public void initialize()
    {
        loadAutoDetectedSerialDevices();
    }

    /* (non-Javadoc)
     * @see gnu.io.CommDriver#getCommPort(java.lang.String, int)
     */
    public CommPort getCommPort(String portName, int portType)
    {
        CommPort rtn = null;
        
        if (portType == CommPortIdentifier.PORT_SERIAL)
        {
            try
            {
                rtn = new KrohSerialPort(portName);
            }
            catch (PortInUseException e)
            {
                logger.error("Port <" + portName + "> is in use.");
            }
        }
        else
        {
            throw new IllegalArgumentException(
                    "Only PORT_SERIAL type is available in this implementation.");
        }
        
        return rtn;
    }

    private void loadAutoDetectedSerialDevices()
    {
        List<String> deviceNames = new ArrayList<String>();
        
        if (OsRegex.WINDOWS_CE.matcher(osName).matches())
        {
            int portId = 1;
            int numFailures = 0;
            while (numFailures < 10)
            {
                if (!registerSerialDevice("COM" + portId++ + ":"))
                {
                    numFailures++;
                }
            }
        } 
        else if (OsRegex.WINDOWS.matcher(osName).matches())
        {
            int portId = 1;
            int numFailures = 0;
            while (numFailures < 10)
            {
                if (!registerSerialDevice("COM" + portId++))
                {
                    numFailures++;
                }
            }
        }
        else if (OsRegex.MAC.matcher(osName).matches())
        {
            String pattern = "(cu\\..+)|(tty\\..+)";
            addDevicesMatchingPattern(devicesDirectory, pattern, deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.SOLARIS.matcher(osName).matches())
        {
            // Solaris devices can be named like /dev/cua/a,
            // /dev/cuaa, /dev/term/a, /dev/term0:
            addDevicesInDir("/dev/term", deviceNames);
            addDevicesInDir("/dev/cua", deviceNames);
            
            String pattern = "(term\\d+)|(cua\\w+)";
            addDevicesMatchingPattern("/dev", pattern, deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.LINUX.matcher(osName).matches())
        {
            String pattern =
                    "(ttyS\\d+)|" +
                    "(ttySA\\d+)|" +    // for the IPAQs
                    "(ttyUSB\\d+)|" +   // for USB frobs
                    "(comx\\d+)|" +     // linux COMMX synchronous serial card
                    "(holter\\d+)|" +   // custom card for heart monitoring
                    "(modem\\d+)|" +    // linux symbolic link to modem.
                    "(rfcomm\\d+)|" +   // bluetooth serial device
                    "(ttyircomm\\d+)|" + // linux IrCommdevices (IrDA serial emu)
                    "(ttycosa\\d+c)|" + // linux COSA/SRP synchronous serial card
                    "(ttycosa\\d+c)|" + // linux COSA/SRP synchronous serial card
                    "(ttyC\\d+)|" +     // linux cyclades cards
                    "(ttyCH\\d+)|" +    // linux Chase Research AT/PCI-Fast serial card
                    "(ttyD\\d+)|" +     // linux Digiboard serial card
                    "(ttyE\\d+)|" +     // linux Stallion serial card
                    "(ttyF\\d+)|" +     // linux Computone IntelliPort serial card
                    "(ttyH\\d+)|" +     // linux Chase serial card
                    "(ttyI\\d+)|" +     // linux virtual modems
                    "(ttyL\\d+)|" +     // linux SDL RISCom serial card
                    "(ttyM\\d+)|" +     // linux PAM Software's multimodem boards
                                        // linux ISI serial card
                    "(ttyMX\\d+)|" +    // linux Moxa Smart IO cards
                    "(ttyP\\d+)|" +     // linux Hayes ESP serial card
                    "(ttyR\\d+)|" +     // linux comtrol cards
                                        // linux Specialix RIO serial card
                    "(ttyS\\d+)|" +     // linux Serial Ports
                    "(ttySI\\d+)|" +    // linux SmartIO serial card
                    "(ttySR\\d+)|" +    // linux Specialix RIO serial card 257+
                    "(ttyT\\d+)|" +     // linux Technology Concepts serial card
                    "(ttyUSB\\d+)|" +   // linux USB serial converters
                    "(ttyV\\d+)|" +     // linux Comtrol VS-1000 serial controller
                    "(ttyW\\d+)|" +     // linux specialix cards
                    "(ttyX\\d+)|";      // linux SpecialX serial card
            
            addDevicesMatchingPattern(devicesDirectory, pattern, deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.QNX.matcher(osName).matches())
        {
            addDevicesMatchingPattern(devicesDirectory, "ser.*", deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else if (OsRegex.IRIX.matcher(osName).matches())
        {
            String pattern =
                    "(ttyc\\d+)|" + // Irix raw character devices
                    "(ttyd\\d+)|" + // Irix basic serial ports
                    "(ttyf\\d+)|" + // Irix serial ports with hardware flow
                    "(ttym\\d+)|" + // Irix modems
                    "(ttyq\\d+)|" + // Irix pseudo ttys
                    "(tty4d\\d+)|" +// Irix RS422
                    "(tty4f\\d+)|" +// Irix RS422 with HSKo/HSki
                    "(midi\\d+)|" + // Irix serial midi
                    "(us\\d+)|";    // Irix mapped interface
            
            addDevicesMatchingPattern(devicesDirectory, pattern, deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else if (OsRegex.FREEBSD.matcher(osName).matches())
        {
            String pattern =
                    "(ttyd\\d+)" + // general purpose serial ports
                    "(cuaa\\d+)" + // dialout serial ports
                    "(ttyA\\d+)" + // Specialix SI/XIO dialin ports
                    "(cuaA\\d+)" + // Specialix SI/XIO dialout ports
                    "(ttyD\\d+)" + // Digiboard - 16 dialin ports
                    "(cuaD\\d+)" + // Digiboard - 16 dialout ports
                    "(ttyE\\d+)" + // Stallion EasyIO (stl) dialin ports
                    "(cuaE\\d+)" + // Stallion EasyIO (stl) dialout ports
                    "(ttyF\\d+)" + // Stallion Brumby (stli) dialin ports
                    "(cuaF\\d+)" + // Stallion Brumby (stli) dialout ports
                    "(ttyR\\d+)" + // Rocketport dialin ports
                    "(cuaR\\d+)" + // Rocketport dialout ports
                    "(stl\\d+)";   // Stallion EasyIO board or Brumby N
                    
            addDevicesMatchingPattern(devicesDirectory, pattern, deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else if (OsRegex.NETBSD.matcher(osName).matches())
        {
            // NetBSD serial ports (like /dev/tty0, or /dev/tty1):
            addDevicesMatchingPattern(devicesDirectory, "tty\\d+", deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else if (OsRegex.HP_UX.matcher(osName).matches())
        {
            // HP-UX serial ports (like /dev/tty0p, or /dev/tty1p):
            addDevicesMatchingPattern(devicesDirectory, "tty\\d+p", deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.UNIXWARE.matcher(osName).matches())
        {
            // UW7/OU8 serial ports (like /dev/tty00s, /dev/tty01s):
            addDevicesMatchingPattern(devicesDirectory, "tty\\d+", deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.OPENSERVER.matcher(osName).matches())
        {
            // OSR5 serial ports (like /dev/tty1A or /dev/tty13A):
            // OSR5 USB-serial ports (like /dev/ttyu1A):
            String pattern = "(tty\\d+A)|(ttyu\\d+A)";
            addDevicesMatchingPattern(devicesDirectory, pattern, deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else if (OsRegex.COMPAQ_UNIX.matcher(osName).matches())
        {
            // Digital Unix serial ports (like /dev/tty0):
            addDevicesMatchingPattern(devicesDirectory, "tty\\d+", deviceNames);
            registerSerialDevices(deviceNames);
        }
        else if (OsRegex.BEOS.matcher(osName).matches())
        {
            addDevicesMatchingPattern(devicesDirectory, "serial.*", deviceNames);
            registerSerialDevices(deviceNames);
        } 
        else
        {
            // Consider all devices in the device directory:
            addDevicesInDir(devicesDirectory, deviceNames);
            registerSerialDevices(deviceNames);
        }
    }
    
    /**
     * Adds all the files (non-directories) in the specified directory 
     * to the supplied list.
     * 
     * @param directory
     *          the directory to look in for files
     * @param listOfDevs
     *          the list to add the files to
     */
    private void addDevicesInDir(String directory, List<String> listOfDevs)
    {
        File dir = new File(directory);
        
        if (dir.exists() && dir.isDirectory())
        {
            for (File file : dir.listFiles())
            {
                if (!file.isDirectory())
                {
                    listOfDevs.add(file.getAbsolutePath());
                }
            }
        }
    }
    
    /**
     * Add all the file (non-directories) in the specified directory
     * to the supplied list if their names are a match to the 
     * supplied regular-expression pattern.
     * 
     * @param directory
     *          the directory to look in for files
     * @param pattern
     *          regular-expression pattern to match against
     * @param listOfDevs
     *          the list to add the files to
     */
    private void addDevicesMatchingPattern(String directory,
                                           String pattern,
                                           List<String> listOfDevs)
    {
        if (!directory.endsWith(File.separator))
        {
            directory += File.separator;
        }
        
        File dir = new File(directory);
        
        if (dir.exists() && dir.isDirectory())
        {
            for (String device : dir.list(new RegexFilter(pattern)))
            {
                listOfDevs.add(directory + device);
            }
        }
    }
    
    /**
     * Tests the supplied device to make sure it is a serial port. Then
     * registers the serial port with the <code>CommPortIdentifier</code> class.
     * 
     * @param deviceName
     *            name of device to add and test
     * 
     * @return true if the device is a serial port and was added to the
     *         <code>CommPortIdentifier</code> class
     */
    private boolean registerSerialDevice(String deviceName)
    {
        logger.trace("Attempting test read on port <" + deviceName + ">.");
        
        if (KrohSerialPort.nativeTestSerialPort(deviceName))
        {
            logger.trace("Adding serial port <" + deviceName + ">.");
            
            CommPortIdentifier.addPortName(deviceName,
                                           CommPortIdentifier.PORT_SERIAL, 
                                           this);
            
            return true;
        }
        else
        {
            logger.trace("Test Read of <" + deviceName + "> failed.");
            
            return false;
        }
    }
    
    /**
     * Tests the supplied devices to make sure they are serial ports. Then
     * registers the serial ports with the <code>CommPortIdentifier</code>
     * class.
     * 
     * @param deviceNames
     *            <code>String[]</code> of serial port device names to add and
     *            test
     */
    private void registerSerialDevices(List<String> deviceNames)
    {
        for (String deviceName : deviceNames)
        {
            registerSerialDevice(deviceName);
        }
    }
    
    /**
     * <code>FilenameFilter</code> implementation that uses a
     * regular-expression to filter file names.
     */
    private static class RegexFilter implements FilenameFilter
    {
        public RegexFilter(String regex)
        {
            this(regex, true);
        }
        
        public RegexFilter(String regex, boolean filesOnly)
        {
            pattern = Pattern.compile(regex);
            this.filesOnly = filesOnly;
        }

        public boolean accept(File dir, String name)
        {
            // Strip path information, search for regex:
            File file = new File(dir + File.separator + name);
            
            if (filesOnly && file.isFile())
            {
                return false;
            }
            else
            {
                return pattern.matcher(file.getName()).matches();
            }
        }

        private final Pattern pattern;
        private final boolean filesOnly;
    }
    
    /**
     * Contains regular-expression <code>Patterns</code> that match OS names.
     * Used in auto-detecting serial device names.
     */
    private static class OsRegex
    {
        public static final Pattern WINDOWS_CE = 
            Pattern.compile("Windows CE", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern WINDOWS = 
            Pattern.compile("Windows.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern MAC = 
            Pattern.compile("Mac OS.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern SOLARIS = 
            Pattern.compile("(Solaris)|(SunOS)", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern LINUX = 
            Pattern.compile("Linux.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern QNX = 
            Pattern.compile("QNX.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern IRIX = 
            Pattern.compile("Irix.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern FREEBSD = 
            Pattern.compile("FreeBSD.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern NETBSD = 
            Pattern.compile("NetBSD.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern HP_UX = 
            Pattern.compile("HP-UX.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern UNIXWARE = 
            Pattern.compile("(UnixWare.*)|(OpenUnix.*)", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern OPENSERVER = 
            Pattern.compile("OpenServer.*", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern COMPAQ_UNIX = 
            Pattern.compile("(Compaq.*UNIX)|(OSF1.*)", Pattern.CASE_INSENSITIVE);
        
        public static final Pattern BEOS = 
            Pattern.compile("BeOS.*", Pattern.CASE_INSENSITIVE);
        
        private OsRegex()
        {
            // Prevent instantiation.
        }
    }
    
    /**
     * Get the directory on the system holding the device handles.
     * 
     * @param os
     *            name of the operating system
     * 
     * @return directory containing device handles on system
     */
    private String getOsDeviceDirectory(String os)
    {
        String deviceDir = null;
        
        if (OsRegex.WINDOWS_CE.matcher(os).matches() ||
                OsRegex.WINDOWS.matcher(os).matches())
        {
            deviceDir = File.separator + ".";
        } 
        else if (OsRegex.MAC.matcher(os).matches() ||
                OsRegex.SOLARIS.matcher(os).matches() ||
                OsRegex.LINUX.matcher(os).matches() ||
                OsRegex.QNX.matcher(os).matches() ||
                OsRegex.IRIX.matcher(os).matches() ||
                OsRegex.FREEBSD.matcher(os).matches() ||
                OsRegex.NETBSD.matcher(os).matches() ||
                OsRegex.HP_UX.matcher(os).matches() ||
                OsRegex.UNIXWARE.matcher(os).matches() ||
                OsRegex.OPENSERVER.matcher(os).matches() ||
                OsRegex.COMPAQ_UNIX.matcher(os).matches())
        {
            deviceDir = File.separator + "dev";
        }
        else if (OsRegex.BEOS.matcher(os).matches())
        {
            deviceDir = File.separator + "dev" + File.separator + "ports";
        } 
        else
        {
            deviceDir = File.separator + "dev";
            
            logger.warn("The driver could not detect the device directory " +
                    "for this OS <" + os + ">. Using <" + deviceDir + 
                    ">. Please report this error.");
        }
        
        return deviceDir;
    }
}
